package com.peg;

import com.peg.dataobjects.ErrorMessage;
import com.peg.exceptions.ParserException;
import com.peg.rules.Action;
import com.peg.rules.Any;
import com.peg.rules.Checkpoint;
import com.peg.rules.Eoi;
import com.peg.rules.FirstOf;
import com.peg.rules.OneOrMore;
import com.peg.rules.Optional;
import com.peg.rules.RegExpRule;
import com.peg.rules.Rule;
import com.peg.rules.RuleCallRule;
import com.peg.rules.Sequence;
import com.peg.rules.StringRule;
import com.peg.rules.Test;
import com.peg.rules.TestNot;
import com.peg.rules.ZeroOrMore;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * @author sad
 */
public class ParserConstructor {

    private Map<String, Rule> rules;
    private ParserInputStream stream;
    private List<ErrorMessage> errors;
    private List<GrammarToken> otherTokens;

    public ParserConstructor(String parserGramar) {
        rules = new HashMap<String, Rule>();
        stream = new ParserInputStream(parserGramar);
        errors = new ArrayList<ErrorMessage>();
        otherTokens = new ArrayList<GrammarToken>();
    }

    public List<GrammarToken> getOtherTokens() {
        return otherTokens;
    }

    public Map<String, Rule> getRules() {
        return rules;
    }

    public List<ErrorMessage> getErrors() {
        return errors;
    }

    private void processError(ParserException exception) {
        int startError = stream.getIndex();
        boolean skipGood = stream.skipUntil(';', '}');
        if (stream.peek(-1) == '}') {
            stream.rewind();
        }

        int length;
        if (exception.getLength() == -1) {
            length = stream.getIndex() - startError;
        } else {
            length = exception.getLength();
        }

        ErrorMessage error = new ErrorMessage(exception.getIndex(), length, exception.getMessage());
        errors.add(error);
        if (skipGood == false) {
            throw new ParserException("Cannot restore from previous error", startError);
        }
    }

    private void addToErrorList(ParserException exception) {
        int length;
        if (exception.getLength() == -1) {
            length = stream.getIndex() - exception.getIndex();
        } else {
            length = exception.getLength();
        }

        ErrorMessage error = new ErrorMessage(exception.getIndex(), length, exception.getMessage());
        errors.add(error);
    }

    public void ruleWalker(PegRuleWalker walker) {
        for (Rule r : rules.values()) {
            processRule(r, walker);
        }
    }

    private void processRule(Rule rule, PegRuleWalker walker) {
        walker.ruleWalk(rule);
        if (shouldGoDeeper(rule)) {
            if (rule.getRules() != null) {
                for (Rule r : (Rule[]) rule.getRules()) {
                    processRule(r, walker);
                }
            }
        }
    }

    public BaseParser createParser() {
        try {
            stream.skipBlank();
            checkStartWithRuleKeyword();
            while (true) {
                try {
                    tryToParseRule();
                    stream.skipBlank();
                } catch (ParserException ex) {
                    processError(ex);
                }
                char c = stream.peek(0);
                if (c == '}') {
                    break;
                }

                if (stream.eof()) {
                    throw new ParserException("Reached end of file while looking for end '}' of main 'Rules' section", stream.getIndex());
                }
            }
        } catch (ParserException ex) {
            addToErrorList(ex);
        }
        checkForErrors();
        if (errors.isEmpty()) {
            BaseParser baseParser = new BaseParser(rules);
            return baseParser;
        } else {
            return null;
        }
    }

    public void checkForErrors() {
        if (!rules.containsKey("StartRule")) {
            ErrorMessage error = new ErrorMessage(0, 1, "Grammar should have main rule \"StartRule\"");
            errors.add(error);
        }
        ruleWalker(new PegRuleWalker() {
            @Override
            public void ruleWalk(Rule rule) {
                if (rule instanceof RuleCallRule) {
                    RuleCallRule cr = (RuleCallRule) rule;
                    if (!ifRuleExist(cr.getRuleToCall())) {
                        ErrorMessage error = new ErrorMessage(rule.getStart(), rule.getLength(), "Rule with name " + cr.getRuleToCall() + " Is not found");
                        errors.add(error);
                    }
                }
            }
        });
    }

    private boolean shouldGoDeeper(Rule rule) {
        if (rule == null || rule instanceof RuleCallRule) {
            return false;
        }
        return true;
    }

    private boolean ifRuleExist(String name) {
        return rules.containsKey(name);
    }

    protected Rule tryToParseRule() {
        stream.skipBlank();
        int startIndex = stream.getIndex();
        String ruleName = stream.readWord();
        if (ruleName == null) {
            throw new ParserException("Rule should start with name", stream.getIndex());
        }
        if (ifRuleExist(ruleName)) {
            throw new ParserException("Rule with name " + ruleName + " already exists", startIndex);
        }
        GrammarToken token = new GrammarToken(startIndex, stream.getIndex() - startIndex, ruleName, GrammarToken.GrammarTokenType.RULE_START);
        otherTokens.add(token);
        stream.skipBlank();
        char colon = stream.next();
        if (colon != ':') {
            stream.rewind();
            throw new ParserException("You should place ':' after ruleName [" + ruleName + "]", stream.getIndex() - 1);
        }

        stream.skipBlank();
        int ruleStart = stream.getIndex();
        Rule rule = tryToParseRuleBody();
        if (rule != null) {
            rules.put(ruleName, rule);
        }

        char c = stream.next();
        if (c != ';') {
            throw new ParserException("Rule must end with ';'. May be you forget to add [] as sequence?", ruleStart);
        }
        return rule;
    }

    private Rule tryToParseRuleBody() {
        stream.skipBlank();
        char c = stream.next();

        if (c == '$') {
            return parseActionRule();
        }
        if (c == '\"') {
            return parseStringRule();
        }
        if (c == '\'') {
            return parseStringIgnoreCase();
        }
        if (c == '@') {
            return parseRegexp();
        }
        if (c == '[') {
            return parseSequence();
        }
        if (c == '*') {
            return parseZeroOrMore();
        }
        if (c == '+') {
            return parseOneOrMore();
        }
        if (c == '&') {
            return parseTest();
        }
        if (c == '!') {
            return parseTestNot();
        }
        if (c == '#') {
            return parseFirstOf();
        }
        if (c == '?') {
            return parseOptional();
        }
        //if (c == '{') {
        //    return parseCheckPoint();
        //}
        if (!Character.isJavaIdentifierPart(c)) {
            throw new ParserException("Unrecognized character " + charToString(c), stream.getIndex() - 1, 1);
        }

        stream.rewind();
        return parseRuleCallRule();
    }

    protected Rule parseRuleCallRule() {
        int start = stream.getIndex();
        String word = stream.readWord();
        if (word == null) {
            throw new ParserException("It seems, there should be rule invocation, but there is no any rule", stream.getIndex());
        }

        Rule rule;
        if (word.equals("EOI")) {
            rule = new Eoi();
        } else if (word.equals("ANY")) {
            rule = new Any();
        } else {
            rule = new RuleCallRule(word);
        }
        int length = stream.getIndex() - start;
        rule.setStart(start);
        rule.setLength(length);
        return rule;
    }

    protected ZeroOrMore parseZeroOrMore() {
        int start = stream.getIndex() - 1;
        char c = stream.next();
        if (c != '(') {
            throw new ParserException(String.format("ZeroOrMore rule should be in following format \"*(...\", but found \"*%s...\"", charToString(c)), stream.getIndex() - 2);
        }

        Rule[] parsedRules = parseListOfRules(')', "ZeroOrMore");
        ZeroOrMore z = new ZeroOrMore(convertRuleArray(parsedRules, start));
        int length = stream.getIndex() - start;
        z.setStart(start);
        z.setLength(length);
        return z;
    }

    protected Checkpoint parseCheckPoint() {
        stream.skipBlank();
        int start = stream.getIndex() - 1;
        Rule[] parsedRules = parseListOfRules('}', "CheckPoint");
        Checkpoint z = new Checkpoint(convertRuleArray(parsedRules, start));
        return z;
    }

    protected OneOrMore parseOneOrMore() {
        int start = stream.getIndex() - 1;
        char c = stream.next();
        if (c != '(') {
            throw new ParserException(String.format("OneOrMore rule should be in following format \"+(...\", but found \"+%s...\"", charToString(c)), stream.getIndex() - 2);
        }

        Rule[] parsedRules = parseListOfRules(')', "OneOrMore");
        OneOrMore z = new OneOrMore(convertRuleArray(parsedRules, start));
        int length = stream.getIndex() - start;
        z.setStart(start);
        z.setLength(length);
        return z;
    }

    protected Test parseTest() {
        int start = stream.getIndex() - 1;
        char c = stream.next();
        if (c != '(') {
            throw new ParserException(String.format("Test rule should be in following format \"&(...\", but found \"&%s...\"", charToString(c)), stream.getIndex() - 2);
        }

        Rule[] parsedRules = parseListOfRules(')', "Test");
        Test z = new Test(convertRuleArray(parsedRules, start));
        int length = stream.getIndex() - start;
        z.setStart(start);
        z.setLength(length);
        return z;
    }

    protected TestNot parseTestNot() {
        int start = stream.getIndex() - 1;
        char c = stream.next();
        if (c != '(') {
            throw new ParserException(String.format("TestNot rule should be in following format \"!(...\", but found \"!%s...\"", charToString(c)), stream.getIndex() - 2);
        }

        Rule[] parsedRules = parseListOfRules(')', "TestNot");
        TestNot z = new TestNot(convertRuleArray(parsedRules, start));
        int length = stream.getIndex() - start;
        z.setStart(start);
        z.setLength(length);
        return z;
    }

    protected Optional parseOptional() {
        int start = stream.getIndex() - 1;
        char c = stream.next();
        if (c != '(') {
            throw new ParserException(String.format("Optional rule should be in following format \"?(...\", but found \"!%s...\"", charToString(c)), stream.getIndex() - 2);
        }

        Rule[] parsedRules = parseListOfRules(')', "Optional");
        Optional z = new Optional(convertRuleArray(parsedRules, start));
        int length = stream.getIndex() - start;
        z.setStart(start);
        z.setLength(length);
        return z;
    }

    private Rule convertRuleArray(Rule[] rules, int index) {
        if (rules == null) {
            throw new ParserException("List of rules cannot be null", index);
        }
        if (rules.length == 0) {
            throw new ParserException("You should provide at least one rule in the (...)", index);
        }
        if (rules.length == 1) {
            return rules[0];
        }
        return new Sequence(rules);
    }

    protected FirstOf parseFirstOf() {
        int start = stream.getIndex() - 1;
        char c = stream.next();
        if (c != '(') {
            throw new ParserException(String.format("FirstOf rule should be in following format \"#(...\", but found \"!%s...\"", charToString(c)), stream.getIndex() - 2);
        }

        Rule[] parsedRules = parseListOfRules(')', "FirstOf");
        FirstOf z = new FirstOf(parsedRules);
        int length = stream.getIndex() - start;
        z.setStart(start);
        z.setLength(length);
        return z;
    }

    protected Sequence parseSequence() {
        int start = stream.getIndex() - 1;
        Rule[] parsedRules = parseListOfRules(']', "Sequence");
        Sequence s = new Sequence(parsedRules);
        int length = stream.getIndex() - start;
        s.setStart(start);
        s.setLength(length);
        return s;
    }

    protected Rule[] parseListOfRules(char validEndOfSequence, String constructionName) {
        stream.skipBlank();
        int ruleStartIndex = stream.getIndex();
        List<Rule> parsedRules = new ArrayList<Rule>();
        while (true) {
            char c = stream.next();
            if (c == validEndOfSequence) {
                return parsedRules.toArray(new Rule[parsedRules.size()]);
            }

            if (stream.eof()) {
                throw new ParserException(String.format("Reached end of file while looking for end of %s", constructionName), ruleStartIndex);
            }

            stream.rewind();
            while (true) {
                Rule rule = tryToParseRuleBody();
                if (rule == null) {
                    throw new ParserException(String.format("Parse %s returned null internal rule", constructionName), ruleStartIndex);
                }

                parsedRules.add(rule);
                stream.skipBlank();
                c = stream.next();
                if (c == validEndOfSequence) {
                    return parsedRules.toArray(new Rule[parsedRules.size()]);
                }

                if (c != ',') {
                    throw new ParserException("You should place ',' between rules in sequence.", stream.getIndex() - 1);
                }
                stream.skipBlank();
            }
        }
    }

    protected Rule parseRegexp() {
        int stringstart = stream.getIndex() - 1;
        char stringStart = stream.next();
        if (stream.eof()) {
            throw new ParserException("Reached end of file while parsing regexp value. You forget to close regexp with [\"] symbol.", stringstart);
        }
        String regexp = readStringUntilEndSymbol('\"', false);
        try {
            Pattern.compile(regexp);
        } catch (Exception ex) {
            throw new ParserException("Regex has wrong syntax", stringstart, stream.getIndex() - stringStart);
        }

        RegExpRule re = new RegExpRule(regexp);
        int length = stream.getIndex() - stringstart;
        re.setStart(stringstart);
        re.setLength(length);
        return re;
    }

    protected Rule parseStringRule() {
        int start = stream.getIndex() - 1;
        String string = readStringUntilEndSymbol('"', true);
        StringRule sr = new StringRule(string, false);
        int length = stream.getIndex() - start;
        sr.setStart(start);
        sr.setLength(length);
        return sr;
    }

    protected Rule parseStringIgnoreCase() {
        int start = stream.getIndex() - 1;
        String string = readStringUntilEndSymbol('\'', true);
        int length = stream.getIndex() - start;
        StringRule sr = new StringRule(string, true);
        sr.setStart(start);
        sr.setLength(length);
        return sr;
    }

    protected Rule parseActionRule() {
        int start = stream.getIndex() - 1;
        String word = stream.readWord();
        if (word == null) {
            throw new ParserException("You should provide action name after $", stream.getIndex());
        }
        int length = stream.getIndex() - start;
        Action action = new Action(word);
        action.setStart(start);
        action.setLength(length);
        return action;
    }

    private char processSpecialSymbols(char c) {
        switch (c) {
            case 't':
                return '\t';
            case 'n':
                return '\n';
            case 'r':
                return '\r';
            case 'f':
                return '\f';
            case '\'':
                return '\'';
            case '\"':
                return '\"';
            case '\\':
                return '\\';
            default:
                throw new ParserException("Unrecognized escaping on symbol [" + charToString(c) + "]", stream.getIndex() - 2);
        }
    }

    protected String readStringUntilEndSymbol(char endSymbol, boolean useEscaping) {
        int startStringIndex = stream.getIndex();
        StringBuilder sb = new StringBuilder();
        while (true) {
            char c = stream.next();
            if (c == endSymbol) {
                return sb.toString();
            }

            if (stream.eof()) {
                throw new ParserException("Reached end of file while parsing string value. You forget to close string with [" + charToString(endSymbol) + "] symbol", startStringIndex);
            }

            if (useEscaping) {
                if (c == '\\') {
                    c = stream.next();
                    c = processSpecialSymbols(c);
                }
            }

            sb.append(c);
        }
    }

    protected void checkStartWithRuleKeyword() {
        stream.skipBlank();
        String ruleKeyword = stream.readWord();
        if (!Utils.equals(ruleKeyword, "Rules")) {
            throw new ParserException("Grammar should start with keyword 'Rules'", stream.getIndex());
        }

        stream.skipBlank();
        char colon = stream.next();
        if (!Utils.equals(':', colon)) {
            throw new ParserException("Your should add ':' after main 'Rules' keyword", stream.getIndex());
        }

        stream.skipBlank();
        char openbracket = stream.next();
        if (!Utils.equals('{', openbracket)) {
            throw new ParserException("Rules section should start with '{' bracket", stream.getIndex());
        }
    }

    protected void checkEndWithRuleKeyword() {
        stream.skipBlank();
        char closeBracket = stream.next();
        if (!Utils.equals('}', closeBracket)) {
            throw new ParserException("Rules section should end with '}' bracket", stream.getIndex());
        }
    }

    private String charToString(char c) {
        switch (c) {
            case '\t':
                return "\\t";
            case '\n':
                return "\\n";
            case '\r':
                return "\\r";
            case '\f':
                return "\\f";
            default:
                return "" + c;
        }
    }
}
